#!/usr/bin/env python
# -*- coding: utf-8 -*-



from pysqlite2 import dbapi2 as sqlite
import os
from os.path import join, exists, isdir, isfile, dirname, abspath, expanduser
from gettext import gettext as _

#import locale
#import calendar
#import cPickle

from configobj import ConfigObj

import gtk
import time
import datetime
import dateutil
from dateutil.parser import *

from gnomeagenda.pluginsystem import *
from Singleton import Singleton


SETTINGSDIR = expanduser("~/.gnome2/agenda")


if not exists(SETTINGSDIR):
	try:
			os.makedirs(SETTINGSDIR, 0744)
	except Exception , msg:
			print 'Could not create user dir (%s):\n %s' % (SETTINGSDIR, msg)
			exit

CALDB = SETTINGSDIR + "/calendar.sqlite"
SETTINGS = SETTINGSDIR + "/settings.ini"
ANOTHER_DAY_FMT = "%d. %b."

DATE_INFO_FMT = "%A, %x"

OFFSET_TIME_DELTA =  datetime.datetime.now().replace(microsecond=0) - datetime.datetime.utcnow().replace(microsecond=0)

class CalendarUpdater:
	def __init__(self,provider):
		#Thread.__init__(self)
		self.provider = provider
	def run(self):
		#print "updating %s" % self.provider.Plugin_Name		
		self.provider.deleteEvents()
		self.provider.clearErrors()
		self.provider.getAllEvents()

class CalCalendar:
	id = ""
	name = ""
	color = ""
	updated_ext = None
	updated_int = datetime.datetime.utcnow().replace(microsecond=0)
	provider = ""
	hidden = False
	
	def __init__(self,id,name,color,updated_ext = None,hidden = False):
		self.id = id
		self.name = name
		self.color = color
		self.updated_ext = updated_ext
		self.hidden = hidden
		pass
	def __getitem__(self,item):
		if(self.__dict__.has_key(item)):
			return self.__dict__[item]
		else:
			raise Exception()
	
class CalCalendar_DB(CalCalendar):
	def __init__(self,row):
		CalCalendar.__init__(self,row[0],row[1],row[2],row[3],row[6])
		self.updated_int = row[4]
		self.provider = row[5]
		
class CalEvent:
	id 	   	   = ""
	title  	   = ""
	start_time = ""
	end_time   = ""
	comment    = ""
	duration   = None
	place 	   = ""
	cal_name   = ""
	color  	   = "#afafaf"
	all_day	   = False
	status 	   = "CONFIRMED"

	def __init__(self,row = None):
		"""initialize the Event Object from the Calendar DB-row"""
		"""Events(
		0: ev_id
		1: calendar_id
		2: start_time
		3: end_time
		4: duration
		5: title
		6: comment)
		7: place
		Calendar(
		8: cal_id
		9: name
		10: color
		11: updated_ext
		12: updated_int
		13: provider)"""
		
		if(not row):
			self.start_time = datetime.datetime.now()
			#self.start_time.
			#TODO: round the date
			self.end_time = self.start_time + datetime.timedelta(hours=1)
		else:
			self.id= row[0]

			self.start_time 	= row[2]
			
		  	self.end_time 		= row[3]
		  	
			self.title = str(row[5])
			self.comment = str(row[6])
			self.place 	   = str(row[7])
			
			self.cal_name = str(row[8])
			try:
				self.color = gtk.gdk.color_parse(str(row[9]))
			except:
				self.color = gtk.gdk.color_parse('#F5AC9C')
		

		self.duration = self.end_time - self.start_time
		self.all_day = (self.duration.seconds == 0) and (self.start_time.time() == datetime.time())
		

	def is_all_day(self):
		return self.all_day
	def is_past(self):
		"""return true if the event is ahead in time"""
		now = datetime.datetime.now()
		return self.end_time < now

	def is_today(self):
		return self.start_time.date() == datetime.date.today()

	def getDay(self):
		"""get the day as str("YYYY-MM-DD")"""
		return self.start_time.date().isoformat()
	def getStartDate(self):
		return self.start_time.date()
	def is_now(self):
		now = datetime.datetime.now()
		return now > self.start_time and now < self.end_time
	def is_confirmed(self):
		return self.status == "CONFIRMED"
"""
		self.appointments.connect('updating-calendars-finished',self.appointments_calendars_updated_cb)
		self.appointments.connect('updating-calendars-start',self.appointments_updating_calendars)
		self.appointments.connect('updating-calendars-tick',self.appointments_updating_calendars)
"""		
class Appointments (Singleton,gobject.GObject):
	__shared_state = {}
	__gsignals__ = {
		'updating-provider' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,[gobject.TYPE_STRING]),
	    'updating-calendars-finished': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,[]),
	    'updating-calendars-start': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,[]),
	    'updating-calendars-tick': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,[gobject.TYPE_FLOAT,gobject.TYPE_FLOAT]),
	}
	def today(self):
		return datetime.datetime.utcnow().date()
	def setCalendarHidden(self,id,active):
		sql = "UPDATE Calendar set hidden = ? where id = ?"
		if (active):
			hidden = 0
		else:
			hidden = 1
		self.cur.execute(sql,(hidden,id))
		self.con.commit()
	
	def __del__(self):
		self.settings.write()
		pass
	def __init__(self,**kwargs):
		print kwargs
		super(Appointments,self).__init__(**kwargs)

		#http://en.wikipedia.org/wiki/Singleton_pattern#Python
		#self.__dict__ = self.__shared_state
		
		if not '_ready' in dir(self):
			
			self.settings = ConfigObj(SETTINGS,write_empty_values=True,create_empty=True)
			#enforce read/write by user, nothing else
			os.chmod(SETTINGS,int("600",8))

			#set default values
			self.settings.setdefault('plugins', {})
			self.settings.setdefault('width', '350')
			self.settings.setdefault('height','500')
			self.settings.setdefault('hide_finished', 'True')
			self.settings.setdefault('refresh', '30')
			self.settings.setdefault('reminder', '15')
			self.settings.setdefault('default_color','AABBCC')
			self.settings.setdefault('active_providers',[])
			self.settings.setdefault('num_days','30')
			self.settings.setdefault('accelerator',"<Alt>F12")
			

			try:
				os.makedirs(SETTINGSDIR)
			except:
				pass
			self.providers = []
	
			self.con = sqlite.connect(CALDB,detect_types=sqlite.PARSE_DECLTYPES)
			self.con.text_factory = str

			self.cur = self.con.cursor()
			
			sqlite.register_converter("timestamp", convert_datetime)
			
			sqlite.register_adapter(datetime.datetime, adapt_datetime)

			try:
				#SQL: href, start_time, end_time, duration, title, comment
				self.cur.execute("create table Events(id str, calendar_id str, start_time timestamp, end_time timestamp, duration str, title str, comment str, place str, PRIMARY KEY (id, calendar_id, start_time) ON CONFLICT REPLACE)")
				self.con.commit()
			except sqlite.OperationalError, message:
				#print "Error: ", message
				pass
			except sqlite.DatabaseError:	# Delete the db file & reconnect
				print "Error in Database. Deleting..."
				self.cur.close()
				self.con.close()
				#os.unlink(CALDB)
				self.con = sqlite.connect(CALDB)
				self.cur = self.con.cursor()
			try:
				#SQL: href, start_time, end_time, duration, title, comment
				#self.cur.execute("create table Events(href str, calname str, start_time date, end_time date, duration str, title str, comment str, PRIMARY KEY (href, start_time) ON CONFLICT REPLACE)")
				self.cur.execute("create table Calendar(id str, name str, color str, updated_ext timestamp, updated_int timestamp, provider str, hidden number, PRIMARY KEY (id) ON CONFLICT REPLACE)")
				self.con.commit()
			except sqlite.OperationalError, message:
				#print "Error: ", message
				pass
			except sqlite.DatabaseError:	# Delete the db file & reconnect
				print "Error in Database. Deleting..."
				self.cur.close()
				self.con.close()
				#os.unlink(CALDB)
				self.con = sqlite.connect(CALDB)
				self.cur = self.con.cursor()
			#get the providers
			self.initializeProviders()
		self._ready = True
	def provider_event_added_cb(self,provider):
		self.emit('updating-calendars-tick',-1,-1)
	def initializeProviders(self):
		self.providers = get_by_capability('provider')
		#operate on a copy of the list, otherwise the indeces get out of snyc
		for provider in self.providers[:]:
			module = provider.__module__
			
			active = provider.__module__ in self.settings['active_providers']
			if(not active):
				print "disabling Provider %s" % provider.__module__
				self.providers.remove(provider)
			else:
				print "Loading config for %s"% module			
				if(module in self.settings['plugins']):
					user_settings = self.settings['plugins'][module]
					#print user_settings
					provider.initPlugin(user_settings)
					provider.connect('notify-progress',self.provider_event_added_cb)
				else:
					self.settings['plugins'][module] = {}
				self.settings['plugins'][module].merge(provider.options)
				
	def getErrors(self):
		errors = []
		for provider in self.providers:
			if(len(provider.getErrors())):
				prov_errors = [provider.Plugin_Name,provider.getErrors()]
				errors.append(prov_errors)
		if(len(errors)):		
			print errors
		return errors
	def clearErrors(self):
		"""Clear all errors of the providers"""
		for provider in self.providers:
			provider.clearErrors()		
	def updateCalendars(self):
		self.commit()
		self.emit('updating-calendars-start')
		###
		### use dbus to check for an active network connection
		###
		"""
		http://people.redhat.com/dcbw/NetworkManager/NetworkManager%20DBUS%20API.txt
		DBUS Service:			"org.freedesktop.NetworkManager"
		DBUS Object Path:		"/org/freedesktop/NetworkManager"
		DBUS Interface:		"org.freedesktop.NetworkManager"


		Name:	status			Retrieve status information about the network state
		Args:	(none)
		Returns:	DBUS_TYPE_STRING			"connecting"	- there is a pending network connection (waiting for a
														DHCP request to complete, waiting for an encryption
														key/passphrase, waiting for a wireless network, etc)
										"connected"	- there is an active network connection
										"scanning"	- there is no active network connection, but NetworkManager
														is looking for an access point to associate with
										"disconnected"	- there is no network connection
		"""
		
		print "updating calendars"
		num_cals = len(self.providers)
		i = 1
		for provider in self.providers:
			self.emit('updating-provider',provider.Plugin_Name)
			t =CalendarUpdater(provider)
			t.run()
			
			i+=1
		print "calendars updated"
		
		for provider in self.providers:
			#if the update was usccessful, then delete the current events and fill the new ones in

			if(provider.update_ok):
				
				#insert the calendars of this provider
				for k,cc in provider.calendarStorage.iteritems():
					self.removeCalendarsFromDB([cc[0]])
					self.insertCalendarLocal(cc[0],cc[1],cc[2],cc[3],provider.__module__,cc[4])
				#now insert the events
				for ce in provider.eventStorage:
					self.insertEventLocal(ce['id'],ce['cal_id'],ce['start'],ce['end'],ce['title'],ce['description'],ce['place'])
				self.commit()
		self.emit('updating-calendars-finished')

	def removeProviderFromDB(self,provider):
		#retrieve the calendar ids this provider offers:
		calendar_ids = self.query("select id from CALENDAR where provider = '%s'" %provider.Plugin_Name)
		calendar_ids_2 = []
		for id in  calendar_ids:
			calendar_ids_2.append(id[0])
		sql = ("DELETE from events where calendar_id in ('%s')" %( "', '".join(calendar_ids_2)))
		self.execute(sql)
		sql = ("DELETE from calendar where provider = '%s'") % provider.Plugin_Name
		self.execute(sql)
	def removeCalendarsFromDB(self,calendar_ids = []):
		sql = "DELETE from events where calendar_id in ('%s')" % ( "', '".join(calendar_ids))
		self.execute(sql)
		#sql = "DELETE from calendar where id in ('%s')" % ( "', '".join(calendar_ids))
		#self.execute(sql)		
	def insertCalendarLocal(self,id,name,color,updated_ext,provider,hidden = False):
		"""Add a calendar to the database"""
		updated_int = self.today()
		if(updated_ext == None):
			updated_ext = updated_int

		sql = "select hidden from calendar where id = :id"
		try:
			hidden = self.query(sql,{'id':id})[0][0]
		except:
			hidden = hidden 

		if(hidden):
			hidden = 1
		else:
			hidden = 0

		#print 'cal: \t%s [%s] - %s, %s - %s [%s], hidden: %s' % (name,color, id,updated_ext,updated_int,provider,hidden)
		#Calendar(id , name , color , updated_ext , updated_int , provider)
		sql = "INSERT INTO Calendar values (?,?,?,?,?,?,?)"
		
		if (len(color) < 6):
			color = "#%s" % self.settings['default_color']
		else:
			if(len(color) == 6):
				color = '#'+color
		try:
			self.cur.execute(sql,(id, name,color,updated_ext,updated_int,provider,hidden))
		except Exception,e:
			print e
			print (id, name,color,updated_ext,updated_int,provider,hidden)
		self.commit()
	
	def insertEventLocal(self,id,calendar_id, start_time, end_time, title, comment="",place = "", status = ""):
		
		
		"""insert events into the database"""
		"""times are eigther:
		Date only       (10):	2008-04-16
		Date and time   (23): 	2008-03-31T20:00:00.000
		Date, time tz  (>23):	2008-03-31T11:00:00.000+02:00

		0: ev_id
		1: calendar_id
		2: start_time
		3: end_time
		4: duration
		5: title
		6: comment)
		Calendar(
		7: cal_id
		8: name
		9: color
		10: status
		11: notify
		"""
		comment = comment.replace("\r","").replace("\n"," ")
		
		sql = "INSERT INTO Events values(?, ?, ?, ?, ?, ?, ?,?)"
		
		row = []
		row.append(id)
		row.append(calendar_id)
		row.append(start_time)
		row.append(end_time)
		row.append("")
		row.append(title)
		row.append(comment)
		row.append(place)
		row.append(None)
		row.append("")
		row.append("#ffffff")

		#try:
		cal_event = CalEvent(row)
		
		#except:
		#	print "Could not add the following:"
		#	print row
			
		#	return
						
		#if(cal_event.is_all_day()):
			#an all day event
			#check if the all day event spans more days than one, and add a local event for each day
		if(cal_event.duration.days > 1):
			#print str(cal_event.duration) + " days:" +title
			while (cal_event.end_time > cal_event.start_time):
				
				if (cal_event.end_time > cal_event.start_time):
					#print " %s - %s" % (cal_event.start_time,cal_event.end_time)
					try:
						self.cur.execute(sql ,(id,calendar_id, cal_event.start_time, cal_event.end_time,str(cal_event.duration), cal_event.title,cal_event.comment,cal_event.place))
					except Exception,e:
						print e
						print row
				cal_event.start_time = (cal_event.start_time + datetime.timedelta(days=1))
		else:		
			#print (id,calendar_id, cal_event.start_time, cal_event.end_time,str(cal_event.duration), cal_event.title,cal_event.comment,cal_event.place)
			try:
				self.cur.execute(sql ,(id,calendar_id, cal_event.start_time, cal_event.end_time,str(cal_event.duration), cal_event.title,cal_event.comment,cal_event.place))
			except Exception,e:
				print e
				print row
	def get_calendar_data(self,id):
		self.cur.execute ("select * from Calendar where id = :calendar_id ", {'calendar_id' : str(id) })
		row = self.cur.fetchone()
		if row == None:
			return None
		else:
			return CalCalendar_DB(row)
		
	def calendar_exists(self,id):
		row = self.get_calendar_data(id)
		return row == None
	def search (self,search=""):
		"""the method to retrieve events""" 
		if not isinstance(search,str):
			search = ""
		search = search.replace('*','%')
		if((not search.startswith('%')) and (not search.startswith(' '))):
			search = '%'+search
		if((search[-1] != '%') and (search[-1] != ' ')):
			search = search+'%'
		day_start_utc = local2utc(datetime.datetime.utcnow().replace(hour=0,minute=0,second=0,microsecond=0)) + datetime.timedelta(seconds=-60)
		query = """SELECT e.*,c.name,c.color FROM Events e,Calendar c WHERE c.hidden = 0 and c.id = e.calendar_id AND (e.end_time >= datetime('%s','localtime') and e.start_time < date('now', '%s days') and e.start_time > date('now', '-1 days') )  AND (title LIKE :search OR comment LIKE :search OR name LIKE :search or place LIKE :search or start_time like :search) and c.provider in ('%s') ORDER BY e.start_time """
		query = query % ( day_start_utc, int(self.settings['num_days']) + 1, "', '".join(self.settings['active_providers']))
	
		self.cur.execute(query,{'search':search})
		#print "sql: %s" % query
		#print "search: %s" % search
		#self.cur.execute(query)
		rows = self.cur.fetchall()
		result = []
		for row in rows:
			result.append(CalEvent(row))
		return result
	def getCalendars(self,provider = None):
		"""
		retrieve a list of calendars
		"""
		if(provider == None):
			sql = "select * from Calendar"
		else:
			sql = "select * from Calendar c where provider = '%s' order by lower(name)" % provider
		return self.query(sql)
	def query(self,sql,param = None):
		"""
		This is used internally
		"""
		#sql= "SELECT * FROM Events e,Calendar c WHERE c.id = e.calendar_id ORDER BY e.start_time LIMIT 999"
		if(param):
			self.cur.execute(sql,param)
		else:
			self.cur.execute(sql)
		return self.cur.fetchall()

	def execute(self,sql,opt = None):
		if(opt):
			self.cur.execute(sql,opt)
		else:
			self.cur.execute(sql)
	def commit(self):
		self.con.commit()
	def set_option(self,path,value):
			"""
			set an option
			"""
			args = path.split('/')
			last = args.pop()
			args.reverse()
			t = self.settings

			while args:
				o = args.pop()
				try:
					t = t[o]
				except:
					t[o] = {}
					t = t[o]
			t[last] = value		
		#self.settings[option] = value
	def get_option(self,path):
		"""
		return an option
		"""
		args = path.split('/')
		args.reverse()
		t = self.settings
		while args:
			o = args.pop()
			#print "retrieving option: %s" % o
			t = t[o]
			#print t
		return t
		#return self.settings[option]


def adapt_datetime(ts):
	"""return the string representation of the given timestamp"""
	ts.replace(microsecond = 0)
	if(ts.utcoffset()):
		ts = (ts.replace(tzinfo=None) - ts.utcoffset())
	if(ts.time() == datetime.time()):
		return ts.date().isoformat()
	else:
		return ts.isoformat(" ")

def convert_datetime(s):
	"""returns database date/times stored in UTC as local times"""
	if(len(s) == 10):
		return dateutil.parser.parse(s)
	else:
		return utc2local(dateutil.parser.parse(s))

def utc2local(dt):
	return datetime.datetime.fromtimestamp(time.mktime(dt.utctimetuple()) - time.timezone)
def local2utc(dt):
	"""Return the UTC value for the given date
	if datetime has no timezone set, it is treated a local time and converted to a utc time"""
	if(dt.utcoffset() or dt.tzinfo):
		dt = (dt.replace(tzinfo=None) - dt.utcoffset())	
		if(dt.tzinfo):
			dt = dt.replace(tzinfo=None)
		return dt
	else:
		return dt - OFFSET_TIME_DELTA
	
def gtime():
	return convert_datetime('2009-04-17T19:00:00.000+02:00')
def jtime():
	return convert_datetime('2009-04-23T13:30:00.000Z')
	
